package com.mars.module.workflow.service.impl;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mars.common.response.PageInfo;
import com.mars.common.util.Convert;
import com.mars.framework.exception.ServiceException;
import com.mars.module.workflow.entity.ProcessDefinition;
import com.mars.module.workflow.mapper.WorkFlowMapper;
import com.mars.module.workflow.request.ProcessDefinitionRequest;
import com.mars.module.workflow.response.DeploymentResponse;
import com.mars.module.workflow.service.IProcessDefinitionService;
import lombok.AllArgsConstructor;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntityImpl;
import org.activiti.engine.repository.Model;
import org.activiti.engine.repository.ProcessDefinitionQuery;
import org.activiti.engine.runtime.ProcessInstance;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipInputStream;

/**
 * 功能描述
 *
 * @author 程序员Mars
 * @version 1.0
 * @date 2024-01-23 18:13:35
 */
@Service
@AllArgsConstructor
public class ProcessDefinitionServiceImpl implements IProcessDefinitionService {

    private final RepositoryService repositoryService;

    private final WorkFlowMapper workFlowMapper;

    private final RuntimeService runtimeService;

    @Override
    public PageInfo<ProcessDefinition> pageList(ProcessDefinitionRequest request) {
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        processDefinitionQuery.orderByProcessDefinitionId().orderByDeploymentId().desc();
        this.buildQuery(request, processDefinitionQuery);
        List<org.activiti.engine.repository.ProcessDefinition> processDefinitionList = processDefinitionQuery.listPage((request.getPageNo() - 1) * request.getPageSize(), request.getPageSize());
        if (CollectionUtils.isEmpty(processDefinitionList)) {
            return new PageInfo<>();
        }
        List<String> deployIds = processDefinitionList.stream().map(org.activiti.engine.repository.ProcessDefinition::getDeploymentId).collect(Collectors.toList());
        List<DeploymentResponse> deploymentResponses = workFlowMapper.selectDeploymentByIds(deployIds);
        Map<String, DeploymentResponse> deploymentResponseMap = deploymentResponses.stream().collect(Collectors.toMap(DeploymentResponse::getId, Function.identity()));
        List<ProcessDefinition> definitionList = getProcessDefinitions(processDefinitionList, deploymentResponseMap);
        List<ProcessDefinition> list = definitionList.stream().sorted(Comparator.comparing(ProcessDefinition::getDeploymentTime).reversed()).collect(Collectors.toList());
        return new PageInfo<>((int) processDefinitionQuery.count(), list);
    }


    private List<ProcessDefinition> getProcessDefinitions(List<org.activiti.engine.repository.ProcessDefinition> processDefinitionList, Map<String, DeploymentResponse> deploymentResponseMap) {
        return processDefinitionList.stream().map(definition -> {
            ProcessDefinitionEntityImpl entityImpl = (ProcessDefinitionEntityImpl) definition;
            ProcessDefinition entity = new ProcessDefinition();
            entity.setId(definition.getId());
            entity.setKey(definition.getKey());
            entity.setName(definition.getName());
            entity.setCategory(definition.getCategory());
            entity.setVersion(definition.getVersion());
            entity.setDescription(definition.getDescription());
            entity.setDeploymentId(definition.getDeploymentId());
            DeploymentResponse deploymentResponse = deploymentResponseMap.get(entity.getDeploymentId());
            if (Objects.nonNull(deploymentResponse)) {
                entity.setDeploymentTime(deploymentResponse.getDeploymentTime());
            }
            entity.setDiagramResourceName(definition.getDiagramResourceName());
            entity.setResourceName(definition.getResourceName());
            entity.setSuspendState(entityImpl.getSuspensionState() + "");
            return entity;
        }).collect(Collectors.toList());
    }

    @Override
    public String getProcessImageBase64(String deploymentId) throws IOException {
        // 根据部署ID获取流程定义对象
        List<org.activiti.engine.repository.ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploymentId)
                .list();
        if (CollectionUtils.isEmpty(list)) {
            throw new ServiceException("获取流程部署列表失败");
        }
        org.activiti.engine.repository.ProcessDefinition processDefinition = list.get(0);
        // 获取流程定义的资源名称（流程图文件名称）
        String resourceName = processDefinition.getDiagramResourceName();
        // 获取流程图文件的输入流
        InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
        // 将输入流转换为字节数组
        byte[] imageBytes = readInputStream(inputStream);
        // 将字节数组转换为Base64编码
        return "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(imageBytes);
    }

    @Override
    public void suspendOrActiveApply(String id, String suspendState) {
        if ("1".equals(suspendState)) {
            // 当流程定义被挂起时，已经发起的该流程定义的流程实例不受影响（如果选择级联挂起则流程实例也会被挂起）。
            // 当流程定义被挂起时，无法发起新的该流程定义的流程实例。
            // 直观变化：act_re_procdef 的 SUSPENSION_STATE_ 为 2
            repositoryService.suspendProcessDefinitionById(id);
        } else if ("2".equals(suspendState)) {
            repositoryService.activateProcessDefinitionById(id);
        }
    }

    @Override
    public void convertToModel(String processDefinitionId) throws UnsupportedEncodingException, XMLStreamException {
        org.activiti.engine.repository.ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(processDefinitionId).singleResult();
        InputStream bpmnStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(),
                processDefinition.getResourceName());
        XMLInputFactory xif = XMLInputFactory.newInstance();
        InputStreamReader in = new InputStreamReader(bpmnStream, "UTF-8");
        XMLStreamReader xtr = xif.createXMLStreamReader(in);
        BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xtr);

        BpmnJsonConverter converter = new BpmnJsonConverter();
        ObjectNode modelNode = converter.convertToJson(bpmnModel);
        Model modelData = repositoryService.newModel();
        modelData.setKey(processDefinition.getKey());
        modelData.setName(processDefinition.getResourceName());
        modelData.setCategory(processDefinition.getDeploymentId());

        ObjectNode modelObjectNode = new ObjectMapper().createObjectNode();
        modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME, processDefinition.getName());
        modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, 1);
        modelObjectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, processDefinition.getDescription());
        modelData.setMetaInfo(modelObjectNode.toString());
        repositoryService.saveModel(modelData);
        repositoryService.addModelEditorSource(modelData.getId(), modelNode.toString().getBytes(StandardCharsets.UTF_8));

    }

    @Override
    public void deleteProcessDeploymentByIds(String deploymentIds) throws Exception {
        String[] deploymentIdsArr = Convert.toStrArray(deploymentIds);
        for (String deploymentId : deploymentIdsArr) {
            List<ProcessInstance> instanceList = runtimeService.createProcessInstanceQuery()
                    .deploymentId(deploymentId)
                    .list();
            if (!CollectionUtils.isEmpty(instanceList)) {
                // 存在流程实例的流程定义
                throw new ServiceException("删除失败，存在运行中的流程实例");
            }
            // true 表示级联删除引用，比如 act_ru_execution 数据
            repositoryService.deleteDeployment(deploymentId, true);
        }
    }

    @Override
    public void deployProcessDefinition(MultipartFile file) throws IOException {
        if (!file.isEmpty()) {
            String extensionName = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.') + 1);
            if (!"bpmn".equalsIgnoreCase(extensionName)
                    && !"zip".equalsIgnoreCase(extensionName)
                    && !"bar".equalsIgnoreCase(extensionName)) {
                throw new ServiceException("流程定义文件仅支持 bpmn, zip 和 bar 格式！");
            }
            String originalFilename = file.getOriginalFilename();
            if (StringUtils.isNotBlank(originalFilename)) {
                String uploadDir = System.getProperty("user.dir") + "\\code";
                Path destination = Paths.get(uploadDir, file.getOriginalFilename());
                Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
                String realFilePath = destination.toAbsolutePath().toString();
                this.deployProcessDefinition(realFilePath);
            }
        }
    }


    /**
     * 部署流程定义
     *
     * @param filePath filePath
     */
    public void deployProcessDefinition(String filePath) throws FileNotFoundException {
        if (StringUtils.isNotBlank(filePath)) {
            if (filePath.endsWith(".zip")) {
                ZipInputStream inputStream = new ZipInputStream(new FileInputStream(filePath));
                repositoryService.createDeployment()
                        .addZipInputStream(inputStream)
                        .deploy();
            } else if (filePath.endsWith(".bpmn")) {
                repositoryService.createDeployment()
                        .addInputStream(filePath, new FileInputStream(filePath))
                        .deploy();
            }
        }
    }

    private byte[] readInputStream(InputStream inputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, length);
        }
        inputStream.close();
        outputStream.close();
        return outputStream.toByteArray();
    }

    private void buildQuery(ProcessDefinitionRequest request, ProcessDefinitionQuery processDefinitionQuery) {
        if (StringUtils.isNotBlank(request.getName())) {
            processDefinitionQuery.processDefinitionNameLike("%" + request.getName() + "%");
        }
        if (StringUtils.isNotBlank(request.getKey())) {
            processDefinitionQuery.processDefinitionKeyLike("%" + request.getKey() + "%");
        }
        if (StringUtils.isNotBlank(request.getCategory())) {
            processDefinitionQuery.processDefinitionCategoryLike("%" + request.getCategory() + "%");
        }
    }
}
